为什么你需要ViewObject

WhyNeedViewObject

WhyNeedViewObject

作者:李旺成
时间:2016年4月12日


这里使用了一个解析当前天气 JSON 字符串得到原始 Model 后,将该 Model 的数据展示到一个简单的页面上来进行演示。

先看下 Demo 的效果图:
天气展示 Demo

天气展示 Demo

我理解的 VO

VOViewObjectViewModel。关于它的解释在 Android MVP 详解(下)中,我做过简要的阐述。这里,再说说我是怎么理解 VO 的。

VO,就是一切给 View 提供数据的对象。这个定义就很广泛了,所以我对 VO 做了如下的分类(下面会细说)。

VO 的实现方式

既然,所有给 View 提供数据的对象都可以称之为 VO,那么 VO 的来源或者说形式就很多了。我在这里根据 VO 的实现方式进行了分类,仅仅是一家之言,有疏漏之处,见谅。

1. 单独的 VO 类

Android MVP 详解(下)中建议专门建一个包 vo,用来存放该模块下的所有 VO 类。对于这一类,那就属于单独的 VO 类,或者更准确的说明是“独立的 VO 类”。

要使用这种类型的 VO,有一个问题,它是独立的类,那么就需要另外的对象给它提供数据。在这里我认为提供(传递)数据的方式,大致有如下两种:

A. 使用转换器

专门使用一个转换器类,来做原始 Model 到 VO 的转换。如示例项目中的 VOConverterUtil.java 类。(在这类里偷了个懒,直接调用了“构造方法中转换”的方式进行了转换)
还是看下代码吧:

1
2
3
4
5
6
7
public class VOConverterUtil {
public static WeatherVO getWeatherVOFromWeatherBean(WeatherBean weatherBean) {
// 这里偷个懒
WeatherVO weatherVO = new WeatherVO(weatherBean);
return weatherVO;
}
}

B. 构造方法中转换

这个很好理解,就是在构造方法中进行数据转换。代码很简单,直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public WeatherVO(WeatherBean weatherBean) {
if (weatherBean == null) return;
isSuccess = "ok".equals(weatherBean.getStatus());
int condCode = Integer.parseInt(weatherBean.getNow().getCond().getCode());
String condCodeColorStr = "";
if (condCode < 0) {
weatherInfoIcon = R.mipmap.ic_snow;
condCodeColorStr = "#000066";
} else if (condCode < 60) {
weatherInfoIcon = R.mipmap.ic_rain;
condCodeColorStr = "#009900";
} else if (condCode < 90) {
weatherInfoIcon = R.mipmap.ic_cloudy;
condCodeColorStr = "#993300";
} else {
weatherInfoIcon = R.mipmap.ic_sunshine;
condCodeColorStr = "#cccc00";
}
weatherInfoText = Html.fromHtml("<font color='"+condCodeColorStr+"'>"+weatherBean.getNow().getCond().getTxt()+"</font>");
relativeHumidity = "相对湿度:" + weatherBean.getNow().getHum();
int tmpInt = Integer.parseInt(weatherBean.getNow().getTmp());
if (tmpInt < 15) {
temperatureIcon = R.mipmap.ic_lowtemperature;
} else if (tmpInt < 33) {
temperatureIcon = R.mipmap.ic_thermophilic;
} else {
temperatureIcon = R.mipmap.ic_hightemperature;
}
airPressure = "气压:" + weatherBean.getNow().getPres();
precipitation = "降水量:" + weatherBean.getNow().getPcpn();
visibility = "能见度:" + weatherBean.getNow().getVis() + " KM";
windDirectionAngle = "风向角度:" + weatherBean.getNow().getWind().getDeg();
windDirection = "风向:" + weatherBean.getNow().getWind().getDir();
windPower = "风力:" + weatherBean.getNow().getWind().getSc();
windSpeed = "风速" + weatherBean.getNow().getWind().getSpd();
}

2. 实现接口成 VO

抽出单独的类,那么就多了一个类, Modle 如果很多的话,那不可避免 VO 的数量也会增加。有些人可能觉得没必要,这增加了项目复杂度(哈哈,任何的设计都有可能造成复杂度上升)。那么,这样,我们抽取一个接口,然后让原始 Model 去实现这个接口 —— 以后就可以“面向接口编程”了。

思路很简单,那么直接上代码吧:
抽取接口 IWeatherVO.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface IWeatherVO {

boolean isSuccess(); // "status": "ok", //接口状态
int getWeatherInfoIcon(); // "code": "100", //天气状况代码 假设 <0 下雪, < 60 雨,大于 >60 < 90 阴, > 90 晴
Spanned getWeatherInfoText(); // "txt": "晴" //天气状况描述 天气的文本描述
String getRelativeHumidity(); // "hum": "20%", //相对湿度(%)
int getTemperatureIcon(); // "tmp": "32", //温度 温度图标
String getAirPressure(); // "pres": "1001", //气压
String getPrecipitation(); // 降水量
String getVisibility(); // "vis": "10", //能见度(km)
String getWindDirectionAngle(); // "deg": "10", //风向(360度)
String getWindDirection(); // "dir": "北风", //风向
String getWindPower(); // "sc": "3级", //风力
String getWindSpeed(); // "spd": "15" //风速(kmph)

}

实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
public class WeatherBean implements IWeatherVO {

// 原始 Modle 中的字段都省略了,具体看源码吧
...

//==========实现 VO 接口==========
@Override
public boolean isSuccess() {
return "ok".equals(status);
}

@Override
public int getWeatherInfoIcon() {
int weatherInfoIcon;
int condCode = Integer.parseInt(getNow().getCond().getCode());
if (condCode < 0) {
weatherInfoIcon = R.mipmap.ic_snow;
} else if (condCode < 60) {
weatherInfoIcon = R.mipmap.ic_rain;
} else if (condCode < 90) {
weatherInfoIcon = R.mipmap.ic_cloudy;
} else {
weatherInfoIcon = R.mipmap.ic_sunshine;
}
return weatherInfoIcon;
}

@Override
public Spanned getWeatherInfoText() {
Spanned weatherInfoText;
int condCode = Integer.parseInt(getNow().getCond().getCode());
String condCodeColorStr = "";
if (condCode < 0) {
condCodeColorStr = "#000066";
} else if (condCode < 60) {
condCodeColorStr = "#009900";
} else if (condCode < 90) {
condCodeColorStr = "#993300";
} else {
condCodeColorStr = "#cccc00";
}
weatherInfoText = Html.fromHtml("<font color='"+condCodeColorStr+"'>"+getNow().getCond().getTxt()+"</font>");
return weatherInfoText;
}

@Override
public String getRelativeHumidity() {
return "相对湿度:" + getNow().getHum();
}

@Override
public int getTemperatureIcon() {
int temperatureIcon;
int tmpInt = Integer.parseInt(getNow().getTmp());
if (tmpInt < 15) {
temperatureIcon = R.mipmap.ic_lowtemperature;
} else if (tmpInt < 33) {
temperatureIcon = R.mipmap.ic_thermophilic;
} else {
temperatureIcon = R.mipmap.ic_hightemperature;
}
return temperatureIcon;
}

@Override
public String getAirPressure() {
return "气压:" + getNow().getPres();
}

@Override
public String getPrecipitation() {
return "降水量:" + getNow().getPcpn();
}

@Override
public String getVisibility() {
return "能见度:" + getNow().getVis() + " KM";
}

@Override
public String getWindDirectionAngle() {
return "风向角度:" + getNow().getWind().getDeg();
}

@Override
public String getWindDirection() {
return "风向:" + getNow().getWind().getDir();
}

@Override
public String getWindPower() {
return "风力:" + getNow().getWind().getSc();
}

@Override
public String getWindSpeed() {
return "风速" + getNow().getWind().getSpd();
}

}

3. 添加方法成 VO

这个就更简单了,那就是连接口都不抽取了,直接提供上述接口中的方法。这里就不赘述了,思路是和上面提取接口一致,所提供的方法,目的就是方便在 View 中直接使用。(这个在 Android MVP 详解(下)中讨论过,略)

4. 没有 VO

没有 VO,那就是根本不使用 VO。如果你的项目是 MVP 的,那么就在 Presenter 中做数据转换的工作,然后提供给 View 展示。

这对于很简单的 Model 和 简单的 View 是没有问题的,如果,Model 很复杂(字段很多,而且不能直接使用),那么 Presenter 的任务就会很重。

这里就不做演示了,很多人应该都在这么用,或者曾经是这么用的。

使用 VO 的好处

上面说了一堆 VO 的实现方式,但是就是没提使用 VO 到底有何益处;或者说 VO 存在的意义。下面就我个人的理解,谈谈我认为 VO 的好处。

统一命名习惯

很多时候数据来源是网络(服务器端),那么这就可能有一个问题。服务器端的命名习惯可能与客户端有很大区别,还有不同服务器端开发的命名习惯也可能不同(如:使用 PHP 开发的服务器程序和使用 Java 开发的服务器程序命名很可能就是不同的)。

简而言之,那就是服务器反给我们的字段和我们项目中的命名习惯不同,很多人说,这没办法啊!总不能让服务器改吧!

是的,客户端和服务器端的命名很难统一,有人会说,不统一就不统一,又不影响使用。确实,不影响使用,但是,我们追求完美不是(先从最基本的命名规范做起,哈哈)。

所以,从这个角度来考虑,我建议原始的 Model 那就按照接口文档来(当然,如果使用 Gson 的话,关于命名不统一还是可以解决的,有兴趣的可以自行 Google)。我们自己针对 View 定义一套 VO,这个可以完全按照我们自己的命名规范来,至少这里是统一的。

解耦 View 和 Model

解耦,这个就不用多说了吧!我都不直接使用你了,这还不是解耦,View 依赖的是 VO,而不再依赖原始的 Modle。关于解耦所带来的优点,这里就不详述了,一搜一堆…

铺平数据结构

铺平数据结构” —— 可以理解为将原来有多级(层级较深)的对象,转换为层级较浅的对象。

我曾在项目中遇到这样一个问题:有很多相似的页面,但是服务器端给的字段都是不同的,这就需要建立多个 Model 来解析服务器给的数据。考虑到页面基本一样,那就不需要提供多个页面了,直接用一个页面,往里面填充不同的数据就可以了。那么,问题来了,这会导致要写很多重复的填充 View 的代码,因为 Model 是不同的。

对于上述的问题,我的解决方案是,将页面中要使用的数据抽取为独立的 VO,该页面只需要从 VO 中获取数据即可。再就是,关于如何建立 Modle 去解析服务器数据的问题。这里,我只建了一个 Modle,将所有使用这个页面的接口中的返回字段都封装到一个 Modle 中。这得益于 Model 中多了字段,并不会影响 JSON 字符串到对象的转换(至少 Gson 是这样的)。

上面说的这个例子,也可以认为是“铺平了数据结构”。

在这个示例 Demo 中,可以很好的演示 —— “铺平数据结构” 。
先看下原始的 JSON 字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"status": "ok",
"now": {
"cond": {
"code": "100",
"txt": "晴"
},
"fl": "30",
"hum": "20%",
"pcpn": "0.0",
"pres": "1001",
"tmp": "32",
"vis": "10",
"wind": {
"deg": "10", //风向(360度)
"dir": "北风", //风向
"sc": "3级", //风力
"spd": "15" //风速(kmph)
}
}
}

看一下,上面的 JSON 字符串,如果需要获取风速,那么需要先访问 now,在访问 wind,然后才能获取到 spd 字段。在代码中就如下:

1
weatherBean.getNow().getWind().getSpd();

而在我们的 VO 中,可以直接取到:

1
2
3
4
// 数据已经转换过了,这里直接可以取到
public String getWindSpeed() {
return windSpeed;
}

减少可能的问题

其实,View 和 Model 的耦合就是一个很大的问题,哈哈,这个确实能解决。

还有一些问题可以得到避免,例如,减少 View 中对 Model 的取值的各种判断(当然 MVP 就能解决),避免 Model 中的数据异常导致 View 崩溃。

这里就不多说这个问题了,等你遇到的时候,自然就知道能够避免哪些问题了。(偷个懒,这个以后有机会再丰富吧)

VO 使用演示

直接看图吧,就不上 GIF 了。

VO Class 演示

VO Class 演示

VO Interface 演示

VO Interface 演示

VO Method 演示

VO Method 演示

小结

没有可以解决一切问题的妙药,no magic。

关于上述 VO 的各种形式,需要根据具体的场景(项目)来区分,当然这也在很大程度上取决于个人的习惯以及项目的大小。

如果是比较大的项目,那么建议直接抽出一个 VO 包来,为每个 View 都提供单独的 VO 对象,这样也可以保证项目的统一性,不会破坏层之间的依赖。

如果是小项目,那么可以混着用,觉得哪种方式使用起来最方便,那就使用哪种吧!

还是那句话,没有一定之规,要依据使用场景来确定。

项目地址:
GitHub

坚持原创技术分享,您的支持将鼓励我继续创作!